﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Xml.Serialization;
using System.Xml;
using System.IO;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using System.Deployment.Application;
using log4net;

namespace WowTourGuideTranslator
{
    class DataProvider
    {
        #region fields

        static ILog log = LogManager.GetLogger(typeof(DataProvider));

        /// <summary>
        /// Instance of the current form for logging
        /// </summary>
        static TourGuideBaseTranslatorForm form;

        /// <summary>
        /// Background worker for progress reporting
        /// </summary>
        static BackgroundWorker worker;

        /// <summary>
        /// Language to translate to
        /// </summary>
        static string language;

        #endregion fields

        #region constructor

        /// <summary>
        /// Initializes the <see cref="TranslationDataProvider"/> class.
        /// </summary>
        static DataProvider()
        {
            TranslateNotes = false;

            UntranslatedDict = new SerializableDictionary<string, string>();
            TranslationsDict = new SerializableDictionary<string, string>();

            QuestDict = new SerializableDictionary<int, WowheadQuestParser>();
            ItemDict = new SerializableDictionary<int, WowheadItemParser>();

            // set default
            DataLanguage = Language.German;
        }

        #endregion constructor

        #region methods

        /// <summary>
        /// Tries to translate the text error. If it doesn't find the text it returns the original 
        /// and adds an entry to the UntranslatedDict_....xml file.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        static internal string TryTranslateFromDict(string text)
        {
            string returnVal = text;

            if (TranslationsDict.TryGetValue(text, out returnVal))
            {
                return returnVal;
            }
            if (TranslationsDict.TryGetValue(text.Trim(), out returnVal))
            {
                return returnVal;
            }
            else
            {
                DataProvider.TranslationFailed(@"Translation failed for ""{0}""!", text);
                if (!UntranslatedDict.ContainsKey(text))
                    UntranslatedDict.Add(text, string.Empty);
                return text;
            }
        }

        /// <summary>
        /// Determines whether the specified text has translation.
        /// If it doesn't find the text it adds an entry to the UntranslatedDict_....xml file.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns>
        /// 	<c>true</c> if the specified text has translation; otherwise, <c>false</c>.
        /// </returns>
        static internal bool HasTranslation(string text)
        {
            bool returnVal = true;

            if (!TranslationsDict.ContainsKey(text))
            {
                returnVal = false;
                if (!UntranslatedDict.ContainsKey(text))
                    UntranslatedDict.Add(text, string.Empty);
            }

            return returnVal;
        }

        /// <summary>
        /// Initializes the class with the specified form.
        /// </summary>
        /// <param name="form">The form.</param>
        static internal void Initialize(TourGuideBaseTranslatorForm form, BackgroundWorker worker)
        {
            DataProvider.form = form;
            DataProvider.worker = worker;
        }

        /// <summary>
        /// Traces the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        static internal void Trace(string message, params object[] values)
        {
            log.DebugFormat(message, values);
            if (DataProvider.form != null)
            {
                DataProvider.form.Trace(message, values);
            }
            else
            {
                System.Diagnostics.Trace.WriteLine(string.Format(message, values));
            }
        }

        /// <summary>
        /// Traces the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        static internal void Warn(string message, params object[] values)
        {
            log.WarnFormat(message, values);
            if (DataProvider.form != null)
            {
                DataProvider.form.Warn(message, values);
            }
            else
            {
                System.Diagnostics.Trace.WriteLine(string.Format(message, values));
            }
        }

        /// <summary>
        /// Traces the failure of the translation the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        static internal void TranslationFailed(string message, params object[] values)
        {
            log.InfoFormat(message, values);
            if (DataProvider.form != null)
            {
                DataProvider.form.TranslationFailed(message, values);
            }
            else
            {
                System.Diagnostics.Trace.WriteLine(string.Format(message, values));
            }
        }

        /// <summary>
        /// Traces the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        static internal void Error(string message, params object[] values)
        {
            log.ErrorFormat(message, values);
            if (DataProvider.form != null)
            {
                DataProvider.form.Error(message, values);
            }
            else
            {
                System.Diagnostics.Trace.WriteLine(string.Format(message, values));
            }
        }

        /// <summary>
        /// Reports the progress.
        /// </summary>
        /// <param name="percentProgress">The progress in percent.</param>
        static public void ReportProgress(int percentProgress)
        {
            if (DataProvider.worker.IsBusy)
                DataProvider.worker.ReportProgress(percentProgress);
        }

        /// <summary>
        /// Reports the progress.
        /// </summary>
        /// <param name="percentProgress">The progress in percent.</param>
        /// <param name="userState">user state.</param>
        static public void ReportProgress(int percentProgress, object userState)
        {
            if (DataProvider.worker.IsBusy)
                DataProvider.worker.ReportProgress(percentProgress, userState);
        }
        #endregion methods

        #region properies

        /// <summary>
        /// Gets or sets a value indicating whether to try to translates 
        /// the notes as a whole.
        /// This only makes sense if you wnat to translate the data in the 
        /// "UntranslatedDict_[lang].xml" file and copy the content to the 
        /// "TranslatedDict_[lang].xml" file afterwards.
        /// </summary>
        /// <value><c>true</c> if [translate notes]; otherwise, <c>false</c>.</value>
        static internal bool TranslateNotes { get; set; }

        /// <summary>
        /// Gets or sets the translations dict.
        /// </summary>
        /// <value>The translations dict.</value>
        static internal SerializableDictionary<string, string> TranslationsDict { get; private set; }

        /// <summary>
        /// Gets or sets the untranslated dict.
        /// </summary>
        /// <value>The untranslated dict.</value>
        static internal SerializableDictionary<string, string> UntranslatedDict { get; private set; }

        /// <summary>
        /// Gets or sets the quest dict.
        /// </summary>
        /// <value>The quest dict.</value>
        static internal SerializableDictionary<int, WowheadQuestParser> QuestDict { get; private set; }

        /// <summary>
        /// Gets or sets the item dict.
        /// </summary>
        /// <value>The item dict.</value>
        static internal SerializableDictionary<int, WowheadItemParser> ItemDict { get; private set; }

        /// <summary>
        /// Gets or sets the NPC dict.
        /// </summary>
        /// <value>The NPC dict.</value>
        //static internal Dictionary<int, WowheadNpcParser> NpcDict { get; private set; }

        /// <summary>
        /// Gets a value indicating whether a cancellation is pending.
        /// </summary>
        /// <value><c>true</c> if a cancellation is pending; otherwise, <c>false</c>.</value>
        static internal bool CancellationPending
        {
            get
            {
                return DataProvider.worker.CancellationPending;
            }
        }

        /// <summary>
        /// Language to translate to
        /// </summary>
        static internal string DataLanguage
        {
            get
            {
                return language;
            }
            set
            {
                if (language != value && !string.IsNullOrEmpty(value))
                {
                    log.DebugFormat(@"DataLanguage set to ""{0}""", value);
                    // safe old lang info
                    if (!string.IsNullOrEmpty(language))
                    {
                        PersistData();
                    }

                    // change lang
                    language = value;

                    TranslationsDict.Clear();
                    UntranslatedDict.Clear();
                    QuestDict.Clear();
                    ItemDict.Clear();

                    // load new Lang info
                    ReadPersistedData();

                    ReadLibBabble();
                }
            }
        }

        /// <summary>
        /// Gets the GAPI language.
        /// </summary>
        /// <value>The gapi language.</value>
        static internal Gapi.Language.Language GapiLanguage
        {
            get
            {
                switch (language)
                {
                    case Language.German:
                        return Gapi.Language.Language.German;
                    case Language.French:
                        return Gapi.Language.Language.French;
                    case Language.Russian:
                        return Gapi.Language.Language.Russian;
                    case Language.Spanish:
                        return Gapi.Language.Language.Spanish;
                    default:
                        return Gapi.Language.Language.German;
                }
            }
        }

        /// <summary>
        /// Reads the lib babble.
        /// </summary>
        static private void ReadLibBabble()
        {
            string libBabblePath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), Properties.Settings.Default.LibBabbleZoneName);

            if (!File.Exists(libBabblePath))
            {
                Trace(@"LibBabble not found! Path: {0}", libBabblePath);
                return;
            }

            Trace(@"Parsing {0}...", Properties.Settings.Default.LibBabbleZoneName);

            TextReader babbleReader = new StreamReader(libBabblePath);

            string libBabbleText = babbleReader.ReadToEnd();

            babbleReader.Close();
            string langString = DataProvider.DataLanguage + DataProvider.DataLanguage.ToUpper();

            string startRegex = @"(?<=GAME_LOCALE\s+==\s+""" + langString + @"""\s+then\s*lib:SetCurrentTranslations\s*{\s*\n)[^}]*(?=})";
            string lineRegex = @"^\s*\[?""?(?<base>[^""]+)""?\]?\s*=\s*\[?""?(?<trans>[^""]+)""?\]?,\s*$";
            Regex regexFindStart = new Regex(startRegex, RegexOptions.Multiline);

            Match startMatch = regexFindStart.Match(libBabbleText);

            Regex regexGetTranslations = new Regex(lineRegex, RegexOptions.Multiline);

            MatchCollection matches = regexGetTranslations.Matches(startMatch.Value);

            foreach (Match match in matches)
            {
                string baseText = match.Groups["base"].Value.Trim();
                string transText = match.Groups["trans"].Value.Trim();

                // falls der Text
                if (DataProvider.TranslationsDict.ContainsKey(baseText))
                    continue;

                DataProvider.TranslationsDict.Add(baseText, transText);
                Trace("Added location pair \"{0}\" and \"{1}\"", baseText, transText);
            }
        }

        /// <summary>
        /// Reads the persisted data.
        /// </summary>
        private static void ReadPersistedData()
        {
            string dataFileDir = GetDataFileDir();

            // untranslated 
            UntranslatedDict = DeserializeXmlData<SerializableDictionary<string, string>>(Path.Combine(dataFileDir, @"UntranslatedDict_" + DataLanguage + @".xml"));

            // translations
            TranslationsDict = DeserializeXmlData<SerializableDictionary<string, string>>(Path.Combine(dataFileDir, @"TranslationsDict_" + DataLanguage + @".xml"));

            // Quest data
            QuestDict = DeserializeXmlData<SerializableDictionary<int, WowheadQuestParser>>(Path.Combine(dataFileDir, @"QuestDict_" + DataLanguage + @".xml"));

            // Item data
            ItemDict = DeserializeXmlData<SerializableDictionary<int, WowheadItemParser>>(Path.Combine(dataFileDir, @"ItemDict_" + DataLanguage + @".xml"));

            // if click once deployed -> check if there is old data to port
            if (ApplicationDeployment.IsNetworkDeployed)
            {
                string oldDataDir = GetOldDataFileDir(); // folder with previous versions data files

                if (log.IsDebugEnabled)
                    log.DebugFormat("Old data dir path is \"{0}\"", oldDataDir);

                if (Directory.Exists(oldDataDir))
                {
                    List<string> filesToDelete = new List<string>();

                    if (log.IsDebugEnabled)
                        log.DebugFormat("Old data dir \"{0}\" found!", oldDataDir);

                    // untranslated 
                    string oldUntranslatedDictPath = Path.Combine(oldDataDir, @"UntranslatedDict_" + DataLanguage + @".xml");
                    AddOldVersionDataToDict(UntranslatedDict, oldUntranslatedDictPath);

                    // translations
                    string oldTranslationsDictPath = Path.Combine(oldDataDir, @"TranslationsDict_" + DataLanguage + @".xml");
                    AddOldVersionDataToDict(TranslationsDict, oldTranslationsDictPath);

                    // Quest data
                    string oldQuestDictPath = Path.Combine(oldDataDir, @"QuestDict_" + DataLanguage + @".xml");
                    AddOldVersionDataToDict(QuestDict, oldQuestDictPath);

                    // Item data
                    string oldItemDictPath = Path.Combine(oldDataDir, @"ItemDict_" + DataLanguage + @".xml");
                    AddOldVersionDataToDict(ItemDict, oldItemDictPath);

                    // perstist the collected data to the new file location
                    PersistData();

                    // delete the old files
                    File.Delete(oldUntranslatedDictPath);
                    File.Delete(oldTranslationsDictPath);
                    File.Delete(oldQuestDictPath);
                    File.Delete(oldItemDictPath);

                    if (log.IsDebugEnabled)
                    {
                        log.DebugFormat("Deleted old data file \"{0}\"", oldUntranslatedDictPath);
                        log.DebugFormat("Deleted old data file \"{0}\"", oldTranslationsDictPath);
                        log.DebugFormat("Deleted old data file \"{0}\"", oldQuestDictPath);
                        log.DebugFormat("Deleted old data file \"{0}\"", oldItemDictPath);
                    }

                    // if the folder is empty now delete it
                    if (Directory.GetFiles(oldDataDir).Length == 0 && Directory.GetDirectories(oldDataDir).Length == 0)
                    {
                        if (log.IsDebugEnabled)
                            log.DebugFormat("Deleting old data folder \"{0}\"", oldDataDir);

                        Directory.Delete(oldDataDir);
                    }
                } 
            }
        }

        /// <summary>
        /// Adds the old version data to the given data dict.
        /// Used because if a new click once version is installed the old data is copied to the ".pre" folder
        /// </summary>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <typeparam name="TValue">The type of the value.</typeparam>
        /// <param name="dictToAddTo">The dict to add to.</param>
        /// <param name="filePath">The file path.</param>
        private static void AddOldVersionDataToDict<TKey, TValue>(SerializableDictionary<TKey, TValue> dictToAddTo, string filePath)
        {
            if (log.IsDebugEnabled)
                log.DebugFormat("AddOldVersionDataToDict from Path: \"{0}\"", filePath);

            // get old data
            SerializableDictionary<TKey, TValue> oldData = DeserializeXmlData<SerializableDictionary<TKey, TValue>>(filePath);

            // add content of old file to actual 
            foreach (KeyValuePair<TKey,TValue> pair in oldData)
            {
                if (!dictToAddTo.ContainsKey(pair.Key))
                {
                    dictToAddTo.Add(pair.Key, pair.Value);
                }
            }
        }

        /// <summary>
        /// Deserializes the XML data to objects.
        /// </summary>
        /// <typeparam name="TData">The type of the data to deserialize.</typeparam>
        /// <param name="filePath">The file path.</param>
        /// <returns></returns>
        private static TData DeserializeXmlData<TData>(string filePath) where  TData : class, new()
        {
            TData data = null;

            // untranslated
            if (File.Exists(filePath))
            {
                try
                {
                    if (log.IsDebugEnabled)
                        log.DebugFormat(@"DeserializeXmlData<{0}> from ""{1}""", typeof(TData), filePath);

                    XmlSerializer serializer = new XmlSerializer(typeof(TData));

                    XmlTextReader reader = new XmlTextReader(filePath);

                    data = (TData)serializer.Deserialize(reader);
                    reader.Close();
                }
                catch (Exception ex)
                {
                    Error(@"Error deserializing {0} {1}", filePath, ex);
                }
            }

            return data ?? new TData();
        }

        /// <summary>
        /// Serializes the data to XML files.
        /// </summary>
        /// <typeparam name="TData">The type of the data.</typeparam>
        /// <param name="data">The data.</param>
        /// <param name="filePath">The file path.</param>
        private static void SerializeXmlData<TData>(TData data, string filePath) where TData : class
        {
            XmlSerializer serializer = new XmlSerializer(typeof(TData));

            XmlWriter writer = XmlTextWriter.Create(filePath, new XmlWriterSettings() { Encoding = Encoding.UTF8, Indent = true, CloseOutput = true });
            serializer.Serialize(writer, data);
            writer.Close();

            if (log.IsDebugEnabled)
                log.DebugFormat(@"SerializeXmlData<{0}> to ""{1}""", typeof(TData), filePath);
        }

        /// <summary>
        /// Persists the data.
        /// </summary>
        public static void PersistData()
        {
            if (log.IsDebugEnabled)
                log.Debug("PersistData");

            string dataFileDir;

            dataFileDir = GetDataFileDir();

            // translations
            SerializeXmlData(TranslationsDict, Path.Combine(dataFileDir, @"TranslationsDict_" + DataLanguage + @".xml"));

            // Quest data
            SerializeXmlData(QuestDict, Path.Combine(dataFileDir, @"QuestDict_" + DataLanguage + @".xml"));

            // Quest data
            SerializeXmlData(ItemDict, Path.Combine(dataFileDir, @"ItemDict_" + DataLanguage + @".xml"));

            // untranslated items
            SerializeXmlData(UntranslatedDict, Path.Combine(dataFileDir, @"UntranslatedDict_" + DataLanguage + @".xml"));

        }

        /// <summary>
        /// Gets the old data file dir of the clickonce deployment.
        /// </summary>
        /// <returns></returns>
        private static string GetOldDataFileDir()
        {
            string oldDataFileDir;

            // if clock once deployed -> get Data from data dir
            if (ApplicationDeployment.IsNetworkDeployed)
            {
                oldDataFileDir = Path.Combine(ApplicationDeployment.CurrentDeployment.DataDirectory, @".pre\LangFiles");

                if (log.IsDebugEnabled)
                    log.DebugFormat(@"GetOldDataFileDir -> ""{0}""", oldDataFileDir);

                return oldDataFileDir;
            }

            return string.Empty;
        }

        /// <summary>
        /// Gets the data file dir from settings or clickonce deployment data.
        /// </summary>
        /// <returns></returns>
        private static string GetDataFileDir()
        {
            string dataFileDir;
            dataFileDir = Properties.Settings.Default.DataFileDir;

            if (!Directory.Exists(dataFileDir))
            {
                Directory.CreateDirectory(dataFileDir);
            }

            // if clock once deployed -> get Data from data dir
            if (ApplicationDeployment.IsNetworkDeployed)
            {
                dataFileDir = Path.Combine( ApplicationDeployment.CurrentDeployment.DataDirectory, @"LangFiles");
            }

            if (log.IsDebugEnabled)
                log.DebugFormat(@"GetDataFileDir -> ""{0}""", dataFileDir);

            return dataFileDir;
        }



        #endregion properies
    }
}
